/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package nsip.components;

import nsip.model.messages.SimulationAboutToStart;
import java.util.HashSet;
import java.util.Set;
import se.sics.tasim.props.ComponentCatalog;
import se.sics.tasim.props.StartInfo;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import nsip.components.ds.StatisticDataBase;
import nsip.components.ds.bi.NormalizedHistory;
import nsip.transport.NsipMessageSucker;
import nsip.transport.NsipTransport;
import nsip.model.RFQSet;
import nsip.model.messages.AcceptedCustomerRFQsBundle;
import nsip.model.messages.CustomerOrderBundle;
import nsip.model.messages.NewDay;
import nsip.model.messages.SupplierOfferBundle;
import nsip.model.messages.SupplierRFQBundle;
import nsip.model.Item;
import nsip.model.DataRecord;
import nsip.model.ItemType;
import nsip.model.messages.EndOfDay;
import se.sics.isl.transport.Transportable;
import se.sics.tasim.aw.Agent;
import se.sics.tasim.aw.Message;
import se.sics.tasim.props.BOMBundle;
import se.sics.tasim.props.OfferBundle;
import se.sics.tasim.props.OrderBundle;
import se.sics.tasim.props.RFQBundle;
import se.sics.tasim.props.SimulationStatus;


import static nsip.components.ds.StatisticType.*;

/**
 *
 * @author nico.rehwaldt
 */
public class Brain extends Agent implements NsipTransport {
    
    private static final Logger logger = Logger.getLogger("Broker");
    
    private ProductionAgent productionAgent;
    private SalesAgent salesAgent;
    private SupplyAgent supplyAgent;
    
    private InformationSystem is;

    private HashMap<Class, List<NsipMessageSucker>> messageSuckerMap;

    private int lastID = 123;
    private List<String> todaysMessagesReceived = new ArrayList(30);

    private Set<Class> initialMessages = new HashSet() {{
        add(BOMBundle.class);
        add(ComponentCatalog.class);
        add(StartInfo.class);
    }};
    
    /**
     * Sets up all components of the system
     */
    private void bootstrap() {
        
        // TODO: Bootstrap bi with some default values
        is = new InformationSystem(this);

        productionAgent = new ProductionAgent(is, this);
        salesAgent = new SalesAgent(is, this);
        supplyAgent = new SupplyAgent(is, this);

        // Set up message sucking queue with
        // all agents and the information system in it
        List<NsipMessageSucker> messageSuckers = new ArrayList() {{
            add(is);
            add(salesAgent);
            add(supplyAgent);
            add(productionAgent);
        }};
        
        messageSuckerMap = new HashMap();
        
        for (NsipMessageSucker sucker: messageSuckers) {
            for (Class msgCls: sucker.getMessageInterests()) {
                if (!messageSuckerMap.containsKey(msgCls)) {
                    messageSuckerMap.put(msgCls, new ArrayList());
                }

                List<NsipMessageSucker> interestedSuckers = messageSuckerMap.get(msgCls);
                interestedSuckers.add(sucker);
            }
        }

        StatisticDataBase statistics = is.getStatistics();
        statistics.addMetric(FACTORY_UTILIZATION, new NormalizedHistory(0.15, 0.5));
        statistics.addMetric(INCOMMING_CUSTOMER_RFQS, new NormalizedHistory(0.15, 200.0));
        statistics.addMetric(FILTERING_SURVIVORS_TO_INCOMMING_RFQS_RATIO, new NormalizedHistory(0.15, 0.15));
        
        statistics.addMetric(FILTERING_SURVIVORS_RFQS_COUNT, new NormalizedHistory(0.15, 20));
        statistics.addMetric(FINAL_RFQSET_TO_BID_ON_SIZE, new NormalizedHistory(0.15, 20));
        statistics.addMetric(ISSUED_SUPPLIER_RFQS_PER_ROUND, new NormalizedHistory(0.15, 20));
    }

    /**
     * Propagates message to all message suckers interested in it
     * @param message
     */
    private void propagateMessage(Object message) {
        List<NsipMessageSucker> suckers = messageSuckerMap.get(message.getClass());
        if (suckers != null) {
            for (NsipMessageSucker sucker: suckers) {
                sucker.receive(message);
            }
        }
    }

    /**
     * Try to reduce number of rfqs by filtering
     * @param rfqs
     */
    private void reduceRFQs(List<Item> rfqs) {

        int rfqCount = rfqs.size();        
        is.getStatistics().getMetric(INCOMMING_CUSTOMER_RFQS).update(rfqCount);
        
        Iterator<Item> iterator = rfqs.iterator();

        // Try to reduce number of rfqs by filtering them through
        // sales and supply agent
        while (iterator.hasNext()) {
            Item rfq = iterator.next();

            if (salesAgent.acceptsRFQ(rfq) &&
                supplyAgent.acceptsRFQ(rfq) &&
                productionAgent.acceptsRFQ(rfq)) {
                
                // Ok, rfq is accepted
            } else {
                iterator.remove();
            }
        }
        
        is.getStatistics().getMetric(FILTERING_SURVIVORS_RFQS_COUNT).update(rfqs.size());
        is.getStatistics().getMetric(FILTERING_SURVIVORS_TO_INCOMMING_RFQS_RATIO).update((0.0 + rfqs.size()) / rfqCount);
    }
    
    /**
     * Performs the bidding on a number of incomming rfqs
     * 
     * @param bundle
     * @param sender
     */
    private void handleIncommingRFQs(RFQBundle bundle, String sender) {
        List<Item> rfqs = new LinkedList<Item>();

        for (int i = 0; i < bundle.size(); i++) {
            Item item = new Item(new DataRecord.CustomerRFQ(
                bundle.getRFQID(i),
                bundle.getProductID(i),
                bundle.getDueDate(i),
                bundle.getQuantity(i),
                bundle.getPenalty(i),
                bundle.getReservePricePerUnit(i),
                sender));

            item.metadata().setInfo(is.getCatalog().getInfo(item.getProductID()));
            
            rfqs.add(item);
        }

        reduceRFQs(rfqs);
        
        List<RFQSet> rfqSets = productionAgent.computeRFQSets(rfqs);

        List<Double> salesScores = new ArrayList(rfqSets.size());
        List<Double> supplyScores = new ArrayList(rfqSets.size());

        double salesMax = 0.0;
        double supplyMax = 0.0;
        
        for (RFQSet rfqSet: rfqSets) {
            rfqSet.getRatings().get(0);

            double salesScore = salesAgent.evaluateRFQSet(rfqSet);
            if (salesScore > salesMax) {
                salesMax = salesScore;
            }
            salesScores.add(salesScore);

            double supplyScore = supplyAgent.evaluateRFQSet(rfqSet);
            if (supplyScore > supplyMax) {
                supplyMax = supplyScore;
            }
            supplyScores.add(supplyScore);
        }

        Iterator<Double> salesScoresIterator = salesScores.iterator();
        Iterator<Double> supplyScoresIterator = supplyScores.iterator();
        
        for (RFQSet rfqSet: rfqSets) {

            rfqSet.addRating(salesScoresIterator.next() / salesMax);
            rfqSet.addRating(supplyScoresIterator.next() / supplyMax);

            double rating = 0.0;
            for (Double e: rfqSet.getRatings()) {
                rating += e;
            }

            rating /= rfqSet.getRatings().size();
            rfqSet.setAggregatedRating(rating);
        }

        // Propagate
        Comparator<RFQSet> evaluationComparator = new Comparator<RFQSet>() {
            public int compare(RFQSet s1, RFQSet s2) {
                // We want better items to be in the front of the list
                return -1 * s1.getAggregatedRating().compareTo(s2.getAggregatedRating());
            }
        };

        // Sort the list
        Collections.sort(rfqSets, evaluationComparator);
        
        if (!rfqSets.isEmpty()) {
            RFQSet set = rfqSets.get(0);

            is.getStatistics()
              .getMetric(FINAL_RFQSET_TO_BID_ON_SIZE)
              .update(set.getRFQs().size());
            
            if (set.getAggregatedRating() > 0.3) {
                logger.log(
                        Level.FINE,
                        "Selected {}/{} rfqs",
                        new Object[] { set.getRFQs().size(), bundle.size() });
                
                // Propagate accepted rfqs in set to all interested partys
                propagateMessage(new AcceptedCustomerRFQsBundle(set));
            }
        }
    }
    
    @Override
    protected void messageReceived(Message message) {
        Transportable content = message.getContent();

        todaysMessagesReceived.add(content.getClass().getSimpleName());

        if (content instanceof RFQBundle) {
            handleIncommingRFQs((RFQBundle) content, message.getSender());
        } else
        
        if (content instanceof OfferBundle) {
            // In TAC SCM only suppliers send offers to manufacturers
            OfferBundle bundle = (OfferBundle) content;
            
            List<Item> offers = new ArrayList<Item>(bundle.size() * 2);
            
            for (int i = 0; i < bundle.size(); i++) {
                Item rfq = is.getSupplierDB().get(ItemType.RFQ, bundle.getRFQID(i));
                Item offer = rfq.copy().update(new DataRecord.SupplierOffer(
                    bundle.getOfferID(i),
                    bundle.getDueDate(i),
                    bundle.getQuantity(i),
                    bundle.getUnitPrice(i)
                ));
                
                offers.add(offer);
            }
            
            propagateMessage(new SupplierOfferBundle(offers));
        } else
            
        if (content instanceof OrderBundle) {
            // In TAC SCM only customers send orders to manufacturers
            OrderBundle bundle = (OrderBundle) content;
            List<Item> orders = new ArrayList<Item>();

            for (int i = 0; i < bundle.size(); i++) {
                Item offer = is.getCustomerDB().get(ItemType.OFFER, bundle.getOfferID(i));
                Item order = offer.copy().update(
                    new DataRecord.CustomerOrder(bundle.getOrderID(i)));
                
                orders.add(order);
                is.getCustomerDB().add(order);
            }
            
            propagateMessage(new CustomerOrderBundle(orders));
        } else
        
        if (content instanceof SimulationStatus) {
            if (initialMessages.isEmpty()) {
                propagateMessage(new EndOfDay());
                logger.fine("[Messages received today] " + todaysMessagesReceived);
                todaysMessagesReceived.clear();
            }
            
            propagateMessage(new NewDay(((SimulationStatus) content).getCurrentDate() + 1));
        }

        // Not yet performed... just propagate message
        else {
            propagateMessage(content);
        }

        if (!initialMessages.isEmpty()) {
            initialMessages.remove(content.getClass());
            if (initialMessages.isEmpty()) {
                propagateMessage(new SimulationAboutToStart());
            }
        }
    }

    @Override
    protected void simulationSetup() {
        logger.info("Bootstraping agent...");
        bootstrap();
    }

    @Override
    protected void simulationFinished() {
        logger.info("Simulation ended");
    }
    
    public void sendMessage(String receiver, Object message) {

        if (message instanceof Transportable) {
            super.sendMessage(receiver, (Transportable) message);
        } else
        if (message instanceof SupplierRFQBundle) {
            
        }
    }

    public synchronized int generateID() {
        return lastID++;
    }

    public void sendMessages(Map<String, ?> messageMap) {
        for (Map.Entry<String, ?> entry: messageMap.entrySet()) {
            sendMessage(entry.getKey(), entry.getValue());
        }
    }
}
